﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using MentalAlchemy.Atomics;

namespace MentalAlchemy.Molecules.MachineLearning
{
	/// <summary>
	/// [molecule]
	/// 
	/// Class for k-NN method with Leave-One-Out strategy to define the neighborhood size.
	/// </summary>
	[Serializable]
	public class KnnLooClassifier : KnnClassifier
	{
		#region - Leave-One-Out implementation (TrainingSample). -
		/// <summary>
		/// Calculate neighborhood size using Leave-One-Out strategy.
		/// </summary>
		/// <returns>Resulting neighborhood size.</returns>
		public int CalculateK(List<TrainingSample> data)
		{
			#region - Calculate number of classes in training data. -
			var clIds = MachineLearningElements.GetDifferentClassesIds(data.ToArray());
			int clCount = clIds.Count;
			#endregion

			#region - Find neighborhood size which provides minimal error on training data. -
			//
			// Find neighborhood size which provides minimal error on training data.
			int size = data.Count;
			int ersCount = size, kVal = 1;
			var ers = new List<int>();
			for (int i = 1; i < size; i+=2)	// ensure that [i] is odd.
			{
				int tempErsCount = GetErrorsCount(data, i, clCount);
				if (tempErsCount < ersCount)
				{
					ersCount = tempErsCount;
					kVal = i;
				}
				ers.Add(tempErsCount);
			} 
			#endregion

			#region - Fix even K value. -
			//
			// Fix even K value.
			if (kVal % 2 == 0)
			{	// if kValue is even
				++kVal;
				if (kVal > size)
				{
					kVal -= 2;
				}
			} 
			#endregion
			return kVal;
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Calculates the number of errors for the given training samples and neighborhood size.
		/// </summary>
		/// <param name="data">Training data set.</param>
		/// <param name="curK">Neighborhood size.</param>
		/// <param name="clCount">Total number of classes.</param>
		/// <returns>Number of errors.</returns>
		public int GetErrorsCount(List<TrainingSample> data, int curK, int clCount)
		{
			int i = 0, count = 0;
			foreach (var entry in data)
			{
				int classId = RecognizeLeaveOneOut(data, i, curK, clCount);
				if (classId != entry.ClassID) { ++count; }
				++i;
			}

			return count;
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Method which defines class ID for the object with index [objIdx] from [data] (excluding the object itself) using provided neighborhood size.
		/// </summary>
		/// <param name="data">Training data set.</param>
		/// <param name="objIdx">Object's index</param>
		/// <param name="curK">Neighborhood size.</param>
		/// <param name="clCount">Total number of classes.</param>
		/// <returns>Class ID.</returns>
		public int RecognizeLeaveOneOut(List<TrainingSample> data, int objIdx, int curK, int clCount)
		{
			var obj = data[objIdx];
			var size = data.Count;

			var dists = new List<float>();	// distances to all objects from [data] sorted in ascending order.
			var clIds = new List<int>();	// IDs of classes sorted in the same order as distances.
			dists.Add(float.MaxValue);
			clIds.Add(-1);

			#region - Calculate distances between objects.
			for (int i = 0; i < size; ++i)
			{
				if (i == objIdx) continue;

				float dist = DistanceMeasure(data[i].Data, obj.Data);
				//float dist = MatrixMath.EuclidianDistance(data[i].Data, obj.Data);

				// insert found distance into the [dists] container.
				for (int j = 0; j < dists.Count; ++j)
				{
					if (dists[j] <= dist) continue;

					dists.Insert(j, dist);
					clIds.Insert(j, data[i].ClassID);
					break;
				}
			}
			#endregion

			return GetWinningClassId(dists, clIds, curK, clCount);
		}

		/// <summary>
		/// Get ID of the class using information about distances between object under consideration and distances to all training samples. The decision is based upon voting among k closest objects.
		/// </summary>
		/// <param name="dists">Array of distances to all training samples.</param>
		/// <param name="clIds">Class IDs of the training samples.</param>
		/// <param name="curK">Neighborhood size.</param>
		/// <returns>ID of the winning class.</returns>
		public int GetWinningClassId(List<float> dists, List<int> clIds, int curK)
		{
			int clCount = VectorMath.Max(clIds.ToArray());
			return GetWinningClassId(dists, clIds, curK, clCount);
		}
		#endregion

		#region - Training & recognition interface methods (TrainingSample). -
		/// <summary>
		/// Train k-NN Leave-One-Out Recognizer.
		/// </summary>
		/// <param name="data">Training data.</param>
		/// <param name="distMeasure">Distance measure method delegate.</param>
		public void Train(List<TrainingSample> data, DistanceMeasure2D distMeasure)
		{
			tData = data;
			DistanceMeasure = distMeasure;
			classCount = MachineLearningElements.CalculateClasses(data.ToArray());
			kValue = CalculateK(tData);
		}
		#endregion

		#region - Utility methods. -
		/// <summary>
		/// Save k-NN recognizer and all its data into a binary file.
		/// </summary>
		/// <param name="filename">Filename.</param>
		public override void Save(string filename)
		{
			IFormatter formatter = new BinaryFormatter();
			Stream stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None);
			formatter.Serialize(stream, this);
			stream.Close();
		}

		/// <summary>
		/// Load k-NN recognizer and all its data from a binary file.
		/// </summary>
		/// <param name="filename">Filename.</param>
		public override void Load(string filename)
		{
			IFormatter formatter = new BinaryFormatter();
			Stream stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.None);

			var knn = (KnnLooClassifier)formatter.Deserialize(stream);
			stream.Close();

			kValue = knn.kValue;
			tData = new List<TrainingSample>();
			if (knn.tData != null)
			{
				foreach (TrainingSample sample in knn.tData){tData.Add(sample);}
			}

			#region - Calculate number of classes in training data. -
			classCount = MachineLearningElements.CalculateClasses(tData.ToArray());
			#endregion
		}
		#endregion
	}
}
